// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.daemon.impl;

import com.intellij.openapi.editor.ex.RangeHighlighterEx;
import com.intellij.openapi.editor.impl.RangeMarkerImpl;
import com.intellij.openapi.editor.markup.RangeHighlighter;
import com.intellij.openapi.util.TextRangeScalarUtil;
import com.intellij.util.containers.ContainerUtil;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;

/**
 * Cache for highlighters scheduled for removal.
 * You call {@link #recycleHighlighter} to put unused highlighter into the cache
 * and then call {@link #pickupHighlighterFromGarbageBin} (if there is a sudden need for fresh highlighter with specified offsets) to remove it from the cache to re-initialize and re-use.
 * In the end, remaining highlighters left in the cache that nobody picked up and reused are disposed.
 * NOT THREAD-SAFE
 */
final class HighlighterRecycler {
  private final Long2ObjectMap<List<HighlightInfo>> incinerator = new Long2ObjectOpenHashMap<>();  // range -> list of highlighters in this range; these are loose highlighters (ones which are generated by some non-managed pass, meaning other than GHP/LIP)

  /** do not instantiate, use {@link #runWithRecycler} instead */
  private HighlighterRecycler() {
  }

  // return true if RH is successfully recycled, false if race condition intervened
  synchronized void recycleHighlighter(@NotNull HighlightInfo info) {
    RangeHighlighterEx highlighter = info.getHighlighter();
    assert !(info.isFromHighlightVisitor() || info.isFromAnnotator() || info.isFromInspection() || info.isInjectionRelated()) : info;
    assert highlighter != null;
    if (UpdateHighlightersUtil.LOG.isDebugEnabled()) {
      UpdateHighlightersUtil.LOG.debug("recycleHighlighter " + info + HighlightInfoUpdaterImpl.currentProgressInfo());
    }
    long range = ((RangeMarkerImpl)highlighter).getScalarRange();
    incinerator.computeIfAbsent(range, __ -> new ArrayList<>()).add(info);
  }

  // null means no highlighter found in the cache
  @Nullable
  synchronized RangeHighlighter pickupHighlighterFromGarbageBin(int startOffset, int endOffset, int layer) {
    long range = TextRangeScalarUtil.toScalarRange(startOffset, endOffset);
    List<HighlightInfo> collection = incinerator.get(range);
    if (collection != null) {
      for (int i = 0; i < collection.size(); i++) {
        HighlightInfo info = collection.get(i);
        RangeHighlighterEx highlighter = info.getHighlighter();
        if (highlighter.isValid() && highlighter.getLayer() == layer) {
          collection.remove(info);
          if (collection.isEmpty()) {
            incinerator.remove(range);
          }
          if (UpdateHighlightersUtil.LOG.isDebugEnabled()) {
            UpdateHighlightersUtil.LOG.debug("pickupHighlighterFromGarbageBin pickedup:" + highlighter + HighlightInfoUpdaterImpl.currentProgressInfo());
          }
          return highlighter;
        }
      }
    }
    return null;
  }
  //
  @NotNull
  private synchronized Collection<? extends HighlightInfo> forAllInGarbageBin() {
    return ContainerUtil.flatten(incinerator.values());
  }

  @Nullable
  RangeHighlighter pickupFileLevelRangeHighlighter(int fileTextLength) {
    return pickupHighlighterFromGarbageBin(0, fileTextLength, DaemonCodeAnalyzerEx.FILE_LEVEL_FAKE_LAYER);
  }

  /**
   * - create {@link HighlighterRecycler},
   * - run {@code consumer} which usually calls {@link HighlighterRecycler#recycleHighlighter} and {@link HighlighterRecycler#pickupHighlighterFromGarbageBin}
   * - and then incinerate all remaining highlighters, or in the case of PCE, release them back to recyclable state
   */
  static void runWithRecycler(@NotNull HighlightingSession session, @NotNull Consumer<? super HighlighterRecycler> consumer) {
    HighlighterRecycler recycler = new HighlighterRecycler();
    consumer.accept(recycler);
    for (HighlightInfo info : recycler.forAllInGarbageBin()) {
      UpdateHighlightersUtil.disposeWithFileLevelIgnoreErrors(info, session);
    }
  }
  synchronized boolean isEmpty() {
    return incinerator.isEmpty();
  }
}
